/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.cloud.autoscaling;

import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.lucene.store.AlreadyClosedException;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.cloud.autoscaling.TriggerEventType;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.SolrResourceLoader;

public class AutoScaling {

  /**
   * Implementation of this interface is used for processing events generated by a trigger.
   */
  public interface TriggerEventProcessor {

    /**
     * This method is executed for events produced by {@link Trigger#run()}.
     *
     * @param event a subclass of {@link TriggerEvent}
     * @return true if the processor was ready to perform actions on the event, false
     * otherwise. If false was returned then callers should assume the event was discarded.
     */
    boolean process(TriggerEvent event);
  }

  /**
   * Interface for a Solr trigger. Each trigger implements Runnable and Closeable interface. A trigger
   * is scheduled using a {@link java.util.concurrent.ScheduledExecutorService} so it is executed as
   * per a configured schedule to check whether the trigger is ready to fire. The {@link AutoScaling.Trigger#setProcessor(AutoScaling.TriggerEventProcessor)}
   * method should be used to set a processor which is used by implementation of this class whenever
   * ready.
   * <p>
   * As per the guarantees made by the {@link java.util.concurrent.ScheduledExecutorService} a trigger
   * implementation is only ever called sequentially and therefore need not be thread safe. However, it
   * is encouraged that implementations be immutable with the exception of the associated listener
   * which can be get/set by a different thread than the one executing the trigger. Therefore, implementations
   * should use appropriate synchronization around the listener.
   * <p>
   * When a trigger is ready to fire, it calls the {@link TriggerEventProcessor#process(TriggerEvent)} event
   * with the proper trigger event object. If that method returns false then it should be interpreted to mean
   * that Solr is not ready to process this trigger event and therefore we should retain the state and fire
   * at the next invocation of the run() method.
   */
  public interface Trigger extends Closeable, Runnable {
    /**
     * Trigger name.
     */
    String getName();

    /**
     * Event type generated by this trigger.
     */
    TriggerEventType getEventType();

    /** Returns true if this trigger is enabled. */
    boolean isEnabled();

    /** Trigger properties. */
    Map<String, Object> getProperties();

    /** Number of seconds to wait between fired events ("waitFor" property). */
    int getWaitForSecond();

    /** Actions to execute when event is fired. */
    List<TriggerAction> getActions();

    /** Set event processor to call when event is fired. */
    void setProcessor(TriggerEventProcessor processor);

    /** Get event processor. */
    TriggerEventProcessor getProcessor();

    /** Return true when this trigger is closed and cannot be used. */
    boolean isClosed();

    /** Set internal state of this trigger from another instance. */
    void restoreState(Trigger old);

    /** Save internal state of this trigger in ZooKeeper. */
    void saveState();

    /** Restore internal state of this trigger from ZooKeeper. */
    void restoreState();

    /**
     * Called when trigger is created but before it's initialized or scheduled for use.
     * This method should also verify that the trigger configuration parameters are correct. It may
     * be called multiple times.
     * @param properties configuration properties
     * @throws TriggerValidationException contains details of invalid configuration parameters.
     */
    void configure(SolrResourceLoader loader, SolrCloudManager cloudManager, Map<String, Object> properties) throws TriggerValidationException;

    /**
     * Called before a trigger is scheduled. Any heavy object creation or initialisation should
     * be done in this method instead of the Trigger's constructor.
     */
    void init() throws Exception;
  }

  /**
   * Factory to produce instances of {@link Trigger}.
   */
  public static abstract class TriggerFactory implements Closeable {
    protected boolean isClosed = false;

    public abstract Trigger create(TriggerEventType type, String name, Map<String, Object> props) throws TriggerValidationException;

    @Override
    public void close() throws IOException {
      synchronized (this) {
        isClosed = true;
      }
    }
  }

  /**
   * Default implementation of {@link TriggerFactory}.
   */
  public static class TriggerFactoryImpl extends TriggerFactory {

    private final SolrCloudManager cloudManager;
    private final SolrResourceLoader loader;

    public TriggerFactoryImpl(SolrResourceLoader loader, SolrCloudManager cloudManager) {
      Objects.requireNonNull(cloudManager);
      Objects.requireNonNull(loader);
      this.cloudManager = cloudManager;
      this.loader = loader;
    }

    @Override
    public synchronized Trigger create(TriggerEventType type, String name, Map<String, Object> props) throws TriggerValidationException {
      if (isClosed) {
        throw new AlreadyClosedException("TriggerFactory has already been closed, cannot create new triggers");
      }
      if (type == null) {
        throw new IllegalArgumentException("Trigger type must not be null");
      }
      if (name == null || name.isEmpty()) {
        throw new IllegalArgumentException("Trigger name must not be empty");
      }
      Trigger t;
      switch (type) {
        case NODEADDED:
          t = new NodeAddedTrigger(name);
          break;
        case NODELOST:
          t = new NodeLostTrigger(name);
        break;
        case SEARCHRATE:
          t = new SearchRateTrigger(name);
        break;
        case METRIC:
          t = new MetricTrigger(name);
        break;
        case SCHEDULED:
          t = new ScheduledTrigger(name);
        break;
        case INDEXSIZE:
          t = new IndexSizeTrigger(name);
          break;
        default:
          throw new IllegalArgumentException("Unknown event type: " + type + " in trigger: " + name);
      }
      t.configure(loader, cloudManager, props);
      return t;
    }

  }

  public static final String AUTO_ADD_REPLICAS_TRIGGER_NAME = ".auto_add_replicas";

  public static final String AUTO_ADD_REPLICAS_TRIGGER_DSL =
      "    {" +
      "        'name' : '" + AUTO_ADD_REPLICAS_TRIGGER_NAME + "'," +
      "        'event' : 'nodeLost'," +
      "        'waitFor' : -1," +
      "        'enabled' : true," +
      "        'actions' : [" +
      "            {" +
      "                'name':'auto_add_replicas_plan'," +
      "                'class':'solr.AutoAddReplicasPlanAction'" +
      "            }," +
      "            {" +
      "                'name':'execute_plan'," +
      "                'class':'solr.ExecutePlanAction'" +
      "            }" +
      "        ]" +
      "    }";

  public static final Map<String, Object> AUTO_ADD_REPLICAS_TRIGGER_PROPS = (Map) Utils.fromJSONString(AUTO_ADD_REPLICAS_TRIGGER_DSL);

  public static final String SCHEDULED_MAINTENANCE_TRIGGER_NAME = ".scheduled_maintenance";

  public static final String SCHEDULED_MAINTENANCE_TRIGGER_DSL =
          "    {" +
          "        'name' : '" + SCHEDULED_MAINTENANCE_TRIGGER_NAME + "'," +
          "        'event' : 'scheduled'," +
          "        'startTime' : 'NOW'," +
          "        'every' : '+1DAY'," +
          "        'enabled' : true," +
          "        'actions' : [" +
          "            {" +
          "                'name':'inactive_shard_plan'," +
          "                'class':'solr.InactiveShardPlanAction'" +
          "            }," +
          "            {" +
          "                'name':'inactive_markers_plan'," +
          "                'class':'solr.InactiveMarkersPlanAction'" +
          "            }," +
          "            {" +
          "                'name':'execute_plan'," +
          "                'class':'solr.ExecutePlanAction'" +
          "            }" +
          "        ]" +
          "    }";

  public static final Map<String, Object> SCHEDULED_MAINTENANCE_TRIGGER_PROPS = (Map) Utils.fromJSONString(SCHEDULED_MAINTENANCE_TRIGGER_DSL);

}
